#!/usr/bin/env python
#----------------------
# -*- coding: UTF-8 -*-
#----------------------

import os, sys
import socket
import subprocess, threading
import datetime, time
from ConfigParser import SafeConfigParser
from urllib import urlopen

try:
	from twisted.application import service, internet
	from twisted.internet import reactor, defer
	from twisted.python import log, logfile
	from twisted.web import http, static	
except:
	print 'ERROR: twisted is not installed'
	sys.exit(-1)	

try:
	from nevow import flat, stan, rend, loaders, appserver
except:
	print 'ERROR: nevow is not installed'
	sys.exit(-1)	

try:    
	from MythTV import MythDB
	from MythTV import MythBE    
	from MythTV import Program
	from MythTV import Record
	from MythTV import FileTransfer
except:
	print 'ERROR: The python bindings are not installed'
	sys.exit(-1)

# -- log level 1: Normal
# -- log level 2: Detailed
# -- log level 3: Debug
logLevel = 3

def wait(seconds, result=None):
	"""Returns a deferred that will be fired later"""
	d = defer.Deferred()
	reactor.callLater(seconds, d.callback, result)
	return d

class NewovPageXml(rend.Page):
	docFactory = None	
	data_title = ''	
	data_mediaItems = {}
	data_navItems = {}
	data_sysinfoItems = {}
	slot_parenturl = '#'
	slot_refreshurl = '#'
	slot_previousurl = '#'
	slot_nexturl = '#'	
	render_onloadset = ''
	render_refreshpage = ''
	render_systeminfoclass = 'font-size:small'	

	def __init__(self, xmlpagename, title, 
		         mediaItems={}, 
		         navItems={}, 
		         sysinfoItems = {},
		         parenturl='#', refreshurl='#', 
		         previousurl='#', nexturl='#', 
		         onloadset='', refreshpage=''):
		rend.Page.__init__(self)
		self.docFactory = loaders.xmlfile(xmlpagename)
		self.data_title = title
		self.data_mediaItems = mediaItems
		self.data_navItems	= navItems
		self.data_sysinfoItems = sysinfoItems
		self.render_parenturl = parenturl
		self.render_refreshurl = refreshurl
		self.render_previousurl = previousurl
		self.render_nexturl = nexturl
		self.render_onloadset = onloadset
		self.render_refreshpage = refreshpage	

class MythRequestHandler(http.Request):	

	# -- "constant" attributes
	BACKEND_SEP = '[]:[]'	
	# -- important for smooth playback: static buffer length
	# -- this value seems to work well with popcorn device		
	BUFFERSIZE = 128*1024
	RECBUFSIZE = 1024 * 1024 * 10
	CHANNEL_INC = 5
	GUIDE_CHANNEL_INC = 10
	RECORDING_INC = 5
	SCHEDULED_INC = 5
	MAX_DESC_LEN = 300	
	LIVETV_PLAYBACK = 'livetvback_'
	RECORDING_PLAYBACK = 'recordingplayback_'

	# -- well known folders
	CHANNELS_FOLDER		= 1
	GUIDE_FOLDER		= 2
	RECORDINGS_FOLDER	= 3
	SCHEDULED_FOLDER	= 4
	VIDEOS_FOLDER		= 5		
	RESET_BACKEND		= 6	

	# -- static methods
	
	def getCurrentRecording(mythbe, recid):
		"""
		get current recording on a specific recorder
		"""
		ret = None    
		cmd = 'QUERY_RECORDER %s[]:[]GET_CURRENT_RECORDING' % (recid)
		if logLevel > 2: log.msg(cmd)
		res = mythbe.backendCommand(cmd).split(MythRequestHandler.BACKEND_SEP)
		if logLevel > 2: log.msg(res)
		if res != None and len(res) > 8 and res[0].upper() != 'BAD':	    
			# -- return the server's path to the file
			ret = res[8]
		if logLevel > 2: log.msg(ret)
		return ret
	getCurrentRecording = staticmethod(getCurrentRecording)

	def waitForStableBasepath(mythbe, recid, interval, timeout):
		"""     
		mythtv changes the filename after starting to record, wait for stabilization
		"""
		ret = None
		basepath = MythRequestHandler.getCurrentRecording(mythbe, recid)
		if basepath != None:
			timeoutCount = 0
			while True:
				time.sleep(interval)
				timeoutCount += interval
				basepathtemp = MythRequestHandler.getCurrentRecording(mythbe, recid)
				if basepath == basepathtemp:
					ret = basepath              
					break
				elif timeoutCount > timeout:
					break
				basepath = basepathtemp
		return ret
	waitForStableBasepath = staticmethod(waitForStableBasepath)

	def stopLiveTv(mythbe, recid):
		"""
		stops live tv on a specific recorder
		"""
		# -- stop live tv
		cmd = 'QUERY_RECORDER %s[]:[]STOP_LIVETV' % (recid)    
		if logLevel > 2: log.msg(cmd)
		res = mythbe.backendCommand(cmd)
		if res != '':
			res = res.split(MythRequestHandler.BACKEND_SEP)
			res = res[0].upper()
		if logLevel > 2: log.msg(res)
		return (res == 'OK')
	stopLiveTv = staticmethod(stopLiveTv)

	# -- instance methods

	def getRecordingWithBasename(self, basename):
		"""
		Returns a Program object matching the basename
		"""
		cmd = 'QUERY_RECORDING BASENAME %s' % (basename)
		if logLevel > 2: log.msg(cmd)
		res = self.mythbe.backendCommand(cmd).split(self.BACKEND_SEP)
		if logLevel > 2: log.msg(res)
		if res[0].upper() == 'ERROR':
			return None
		else:
			return Program(res[1:], db=self.mythbe.db)

	def startLiveTv(self, channum):
		"""
		starts live tv for a specific channel
		"""
		ret = None
		# -- announce plyback
		cmd = 'ANN Playback %s 0' % (self.hostname)
		if logLevel > 2: log.msg(cmd)		
		res = self.mythbe.backendCommand(cmd)
		if logLevel > 2: log.msg(res)		
		if res != '':
			res = res.split(self.BACKEND_SEP)
			res = res[0].upper()
		if res != 'OK':	
			log.err("ANN Playback failed: %s" % res)			
		else:			
			freerec = None
			try:
				freerec = self.mythbe.getFreeRecorderList()
				if logLevel > 2: log.msg(freerec)
			except:
				log.err("getFreeRecorderList failed")
				freerec = None							
			if freerec != None and len(freerec) > 0:
				recid = str(freerec[0])				
				for recidtemp in self.recPreference:
					if recidtemp in freerec:
						recid = str(recidtemp)
						break;			
				# -- create chain id
				now = datetime.datetime.now()
				starttime = '%04d-%02d-%02dT%02d:%02d:%02d' % (now.year, now.month, now.day, now.hour, now.minute, now.second)
				chainid = 'live-%s-%s' % (self.hostname, starttime)	    
				cmd = 'QUERY_RECORDER %s[]:[]SPAWN_LIVETV[]:[]%s[]:[]0[]:[]%s' % (recid, chainid, channum)
				if logLevel > 2: log.msg(cmd)
				res = self.mythbe.backendCommand(cmd).split(self.BACKEND_SEP)
				if logLevel > 2: log.msg(res)
				if res[0].upper() == 'OK':		
					cmd = 'QUERY_RECORDER %s[]:[]FRONTEND_READY' % (recid)
					if logLevel > 2: log.msg(cmd)
					res = self.mythbe.backendCommand(cmd)
					if logLevel > 2: log.msg(res)
					cmd = 'QUERY_RECORDER %s[]:[]IS_RECORDING' % (recid)
					if logLevel > 2: log.msg(cmd)
					res = self.mythbe.backendCommand(cmd)
					if logLevel > 2: log.msg(res)
					if res[0].upper() == '1':
						ret = recid			
		return ret
	
	def startLiveTvReaderThread(self, recid):		
		readthread = ReadThread(self.channel.factory, self.clientip, recid, self.BUFFERSIZE)
		self.channel.factory.readthreads[self.clientip] = readthread				
		readthread.start()
	
	def stopLiveTvReaderThread(self):
		if self.channel.factory.recids.has_key(self.clientip):
			recid = self.channel.factory.recids[self.clientip]
			del(self.channel.factory.recids[self.clientip])
			if self.channel.factory.readthreads.has_key(self.clientip):
				self.channel.factory.readthreads[self.clientip].stop()
				self.channel.factory.readthreads[self.clientip].join()
				if self.channel.factory.initbytes.has_key(recid):
					del(self.channel.factory.initbytes[recid])
				if self.channel.factory.databytes.has_key(recid):
					del(self.channel.factory.databytes[recid])
			else:
				MythRequestHandler.stopLiveTv(self.mythbe, recid)
				del(self.channel.factory.recids[self.clientip])
			
	def getRangeStartAndEnd(self, headers):
		rangestart = 0L
		rangeend = 0L		
		if headers.has_key('range'):
			rangeHeader = headers['range']			
			rangeValue = rangeHeader.split('=')[1]			
			rangeSplit = rangeValue.split('-')			
			rangestart = long(rangeSplit[0])					
			# -- popcorn never sends a range end...
			if len(rangeSplit) > 1:
				if len(rangeSplit[1]) > 0:
					rangeend = long(rangeSplit[1])			
		if rangeend == 0:
			# -- important for smooth playback: static buffer length
			# -- this value seems to work well with popcorn device
			rangeend = rangestart + self.BUFFERSIZE / 4
			#rangeend = rangestart + self.BUFFERSIZE
		return (rangestart, rangeend)		

	def writeChunk(self, buff, rangestart):		
		# -- get total length
		chunksize = len(buff)		
		# -- prepare headers				
		self.setHeader('Accept-Ranges', 'bytes')	
		contentRange = 'bytes %s-%s/%s' % (rangestart, (rangestart+chunksize), (rangestart+chunksize))
		if logLevel > 1: log.msg(contentRange)
		self.setHeader('Content-Range', contentRange)
		self.setHeader('Content-Length', chunksize)
		self.setResponseCode(http.PARTIAL_CONTENT)
		# -- write data
		self.write(buff)					
		return chunksize	

	def writeChunks(self, buffers, rangestart):	
		chunksize = 0
		# -- get total length
		for buff in buffers:
			chunksize += len(buff)
		# -- prepare headers
		self.setHeader('Accept-Ranges', 'bytes')					
		contentRange = 'bytes %s-%s/%s' % (rangestart, (rangestart+chunksize), (rangestart+chunksize))
		if logLevel > 1: log.msg(contentRange)
		self.setHeader('Content-Range', contentRange)
		self.setHeader('Content-Length', chunksize)					
		self.setResponseCode(http.PARTIAL_CONTENT)
		# -- write data
		for buff in buffers:
			self.write(buff)		
		return chunksize

	def getChannels(self, foldernr, fromnum=0, tonum=sys.maxint, includeDescription=True):
		ret = []
		# -- grab channels from Myth backend
		ch = self.mythdb.getChannels()
		if ch is None:
			empty = { 'tvid': '', 'name': '', 'href': '#', 'descr': 'No channels found' }
			ret.append(empty)
			atend = True
		else:
			# -- collect all channels
			chtemp = {}
			chmax = 0	    
			for c in ch:
				chn = int(c.channum)
				if chn > chmax:
					chmax = chn
				chtemp[chn] = c
			# -- add the channel containers in the correct order
			now = datetime.datetime.now()	    
			chNumber = 0
			for i in range(1, chmax):		
				if chtemp.has_key(i):
					c = chtemp[i]
					chNumber += 1
					if chNumber >= fromnum and chNumber < tonum:
						chName = c.callsign
						chDescr = ''
						glst = self.mythdb.searchGuide(chanid=c.chanid, startbefore=now, endafter=now)
						if glst != None:			
							for g in glst:						
								durationtotal = str((g.endtime - g.starttime).seconds / 60)
								durationsofar = str((now - g.starttime).seconds / 60)			    
								chName = '%s - %s (%s of %s minutes)' % (c.callsign, g.title, durationsofar, durationtotal)
								if includeDescription:
									if g.description != None and len(g.description) > 0:								
										chDescr = g.description[0:self.MAX_DESC_LEN]
						chHref = ('/%d/%s?chanid=%d&parentfromnum=%d' % (foldernr, c.channum, c.chanid, fromnum))					
						chItem = { 'tvid': str(chNumber), 'name': chName, 'href': chHref, 'descr': chDescr }
						ret.append(chItem)
			atend = tonum > len(ch)
		return (ret, atend)

	def getGuide(self, chanid, fromnum, tonum):
		ret = []		
		# -- grab guide from Myth backend
		now = datetime.datetime.now()
		glst = self.mythdb.searchGuide(chanid=str(chanid), endafter=now)
		showNumber = 0	
		if glst is None:
			empty = { 'tvid': '', 'name': '', 'href': '#', 'descr': 'No shows found' }
			ret.append(empty)
			atend = True
		else:
			for g in glst:
				showNumber += 1			
				if showNumber >= fromnum and showNumber < tonum:				
					durationtotal = str((g.endtime - g.starttime).seconds / 60)
					showName = '%s (%s minutes)' % (g.title, durationtotal)				
					beg = g.starttime
					end = g.endtime
					stime = '%04d-%02d-%02d %02dh%02d' % (beg.year, beg.month, beg.day, beg.hour, beg.minute)
					etime = '%02dh%02d' % (end.hour, end.minute)
					showDescr = '%s - %s' % (stime, etime)
					if g.description != None and len(g.description) > 0:								
						showDescr = '%s - %s: %s' % (stime, etime, g.description[0:self.MAX_DESC_LEN])
					showHref = '%s/%04d%02d%02d%02d%02d?chanid=%d' % (self.path, beg.year, beg.month, beg.day, 
										                              beg.hour, beg.minute, chanid)
					showItem = { 'tvid': str(showNumber), 'name': showName, 'href': showHref, 'descr': showDescr }
					ret.append(showItem)
			atend = tonum > len(glst)
		return (ret, atend)

	def getShow(self, chanid, channum, starttime):
		ret = []		
		# -- grab show from Myth backend (chanid and starttime should identify one specific show)
		glst = self.mythdb.searchGuide(chanid=str(chanid), starttime=starttime)		
		if glst != None and len(glst) > 0:
			g = glst[0]
			durationtotal = str((g.endtime - g.starttime).seconds / 60)
			showName = '%s (%s minutes)' % (g.title, durationtotal)
			beg = g.starttime
			end = g.endtime
			stime = '%04d-%02d-%02d %02dh%02d' % (beg.year, beg.month, beg.day, beg.hour, beg.minute)
			etime = '%02dh%02d' % (end.hour, end.minute)
			showDescr = '%s - %s' % (stime, etime)
			if g.description != None and len(g.description) > 0:								
				showDescr = '%s - %s: %s' % (stime, etime, g.description)
			showItem = { 'tvid': '', 'name': showName, 'href': '#', 'descr': showDescr }
			ret.append(showItem)
			now = datetime.datetime.now()			
			if beg <= now and end >= now:
				title = 'Live TV'
				showHref = '/%d/%s' % (self.CHANNELS_FOLDER, channum)				
				showItem = { 'tvid': 'PLAY', 'name': title, 'href': showHref, 'descr': '' }
				ret.append(showItem)			
			records = self.mythdb.searchRecord(chanid=chanid, starttime=starttime)
			if records is None:
				title = 'Record'
				showHref = '%s?chanid=%d&record=yes' % (self.path, chanid)
			else:
				title = 'Cancel Record'
				showHref = '%s?chanid=%d&record=no' % (self.path, chanid)
			showItem = { 'tvid': '', 'name': title, 'href': showHref, 'descr': '' }
			ret.append(showItem)
		return ret		

	def getRecordings(self, fromnum, tonum):
		ret = []
		# -- grab recordings from Myth backend
		records = self.mythdb.searchRecorded()
		if records is None:
			empty = { 'tvid': '', 'name': '', 'href': '#', 'descr': 'No recordings found' }
			ret.append(empty)
			atend = True
		else:
			recNumber = 0
			recRange = range(0, len(records))
			recRange.reverse()
			for iRec in recRange:
				rec = records[iRec]
				recNumber += 1			
				if recNumber >= fromnum and recNumber < tonum:
					#hrefRec = '%s%s' % (self.recRootPath, rec.basename)
					hrefRec = 'http://%s:%s/%s%s' % (self.hostname, self.port, self.RECORDING_PLAYBACK, rec.basename)
					desc = '' if rec.description is None else rec.description[0:self.MAX_DESC_LEN]
					recItem = { 'tvid': str(recNumber), 'name': rec.title, 'href': hrefRec, 'descr': desc }					
					ret.append(recItem)
			atend = tonum > len(records)
		return (ret, atend)

	def getScheduled(self, fromnum=1, tonum=sys.maxint):
		ret = []
		# -- grab recordings from Myth backend
		records = self.mythdb.searchRecord()
		if records is None:
			empty = { 'tvid': '', 'name': '', 'href': '#', 'descr': 'No scheduled recordings found' }
			ret.append(empty)
			atend = True
		else:
			recNumber = 0			
			recRange = range(0, len(records))
			recRange.reverse()
			for iRec in recRange:				
				rec = records[iRec]
				recNumber += 1			
				if recNumber >= fromnum and recNumber < tonum:
					beg = datetime.datetime(rec.startdate.year, rec.startdate.month, rec.startdate.day) + rec.starttime			
					stime = '%04d%02d%02d%02d%02d' % (beg.year, beg.month, beg.day, beg.hour, beg.minute)						
					callsign = self.getCallsign(rec.chanid)					
					title = '%s %s %s - click to cancel' % (callsign, rec.title, str(beg))									
					cancelurl = '%s?chanid=%d&stime=%s' % (self.path, rec.chanid, stime)
					desc = '' if rec.description is None else rec.description[0:self.MAX_DESC_LEN]					
					recItem = { 'tvid': str(recNumber), 'name': title, 'href': cancelurl, 'descr': desc }				
					ret.append(recItem)
			atend = tonum > len(records)
		return (ret, atend)

	def getVideos(self):
		return ''	

	def getStatus(self):		
		ret = []
		# -- free space
		freeSpaceSummary = self.mythbe.getFreeSpaceSummary()
		percent = freeSpaceSummary[1] / (freeSpaceSummary[0] / 100)
		ret.append({ 'title': 'Available disk space', 'info': ('%d MB' % freeSpaceSummary[0]) })
		ret.append({ 'title': 'Used disk space', 'info': ('%d MB (%d percent)' % (freeSpaceSummary[1], percent)) })		
		# -- uptime
		uptime = long(self.mythbe.getUptime())
		upd = datetime.timedelta(0, uptime)
		ret.append({ 'title': 'Uptime', 'info': ('%s' % str(upd)) })
		# -- recorders
		recList = self.mythbe.getRecorderList()
		for recid in recList:
			if self.mythbe.isRecording(recid):
				prog = self.mythbe.getCurrentRecording(recid)
				ret.append({ 'title': 'Recorder %d' % recid, 'info': 'is recording "%s" on %s' % (prog.title, prog.callsign) })
			else:
				ret.append({ 'title': 'Recorder %d' % recid, 'info': 'is free' })
		# -- load
		load = self.mythbe.getLoad()
		ret.append({ 'title': 'Average load 1, 5, 15 minute(s)', 'info': '%f %%, %f %%, %f %%' % load })		
		return ret

	def processRoot(self, filename):
		title = 'MythPoppy ' + self.hostname + ' - ' + 'Home'
		mediaItems = []
		navItems = [			
			{ 'tvid': str(self.CHANNELS_FOLDER), 'name': 'Live Channels', 'href': ('/%d' % self.CHANNELS_FOLDER), 'descr': '' } , 
			{ 'tvid': str(self.GUIDE_FOLDER), 'name': 'Program Guide', 'href': '/' + str(self.GUIDE_FOLDER), 'descr': '' } , 
			{ 'tvid': str(self.RECORDINGS_FOLDER), 'name': 'Recorded Shows', 'href': '/' + str(self.RECORDINGS_FOLDER), 'descr': '' },
			#{ 'tvid': str(self.VIDEOS_FOLDER), 'name': 'Videos', 'href': '/' + str(self.VIDEOS_FOLDER), 'descr': '' },
			{ 'tvid': str(self.SCHEDULED_FOLDER), 'name': 'Scheduled Recordings', 'href': '/' + str(self.SCHEDULED_FOLDER), 'descr': '' }
			#{ 'tvid': str(self.RESET_BACKEND), 'name': 'Reset Backend', 'href': '/' + str(self.RESET_BACKEND), 'descr': '' }
		]
		sysinfoItems = self.getStatus()
		self.write(NewovPageXml(filename, title, mediaItems, navItems, sysinfoItems, 
				                '/', self.path, onloadset=str(self.CHANNELS_FOLDER)).renderSynchronously())

	def processChannelsFolder(self, filename):
		title = 'MythPoppy %s - Channels' % self.hostname			
		# -- parse query string
		if self.args.has_key('fromnum'):
			fromnum = int(self.args['fromnum'][0])			
		else:
			fromnum = 1
		if self.args.has_key('onloadset'):
			onloadset = self.args['onloadset'][0]
		else:
			onloadset = 'next'
		# -- prepare previous / next url
		tonum = fromnum + self.CHANNEL_INC
		if fromnum > self.CHANNEL_INC:
			previousurl = '%s?fromnum=%d&onloadset=previous' % (self.path, fromnum - self.CHANNEL_INC)
		else:
			previousurl = self.path
		nexturl =  '%s?fromnum=%d&onloadset=next' % (self.path, tonum)
		# -- get channels and render page
		mediaItems = []
		(navItems, atend) = self.getChannels(self.CHANNELS_FOLDER, fromnum, tonum)
		if atend:
			nexturl = '#'
		self.write(NewovPageXml(filename, title, mediaItems, navItems, {}, 
				                '/', self.path, previousurl, nexturl, onloadset).renderSynchronously())

	def processChannelItem(self, filename):
		parentfromnum = self.args['parentfromnum'][0] if self.args.has_key('parentfromnum') else 0		
		if not self.args.has_key('recid'):
			self.processChannelItemStartLiveTv(parentfromnum)
		else:
			recid = self.args['recid'][0]
			self.processChannelItemRefresh(filename, recid, parentfromnum)

	def processChannelItemStartLiveTv(self, parentfromnum):
		# -- start live tv
		splitpath = self.path.split('/')
		channum = splitpath[-1]				
		recid = self.startLiveTv(channum)		
		basepath = MythRequestHandler.waitForStableBasepath(self.mythbe, recid, interval=2, timeout=30)
		if basepath != None:
			# -- store recid and basepath
			self.channel.factory.recids[self.clientip] = recid			
			self.channel.factory.basepaths[self.clientip] = basepath
			# -- start reader thread			
			self.channel.factory.initbytes[recid] = []
			self.channel.factory.databytes[recid] = []
			self.channel.factory.byteswritten[recid] = 0
			self.startLiveTvReaderThread(recid)
			# -- prepare basename	
			splitpath = basepath.split('/')						
			basename = splitpath[-1]			
			
			redirurl = '%s?recid=%s&parentfromnum=%s' % (self.path, recid, parentfromnum)
			self.redirect(redirurl.encode('utf8'))
		else:
			MythRequestHandler.stopLiveTv(self.mythbe, recid)			
			self.write('<h1>Error</h1>Waiting for stable base path failed')	

	def processChannelItemRefresh(self, filename, recid, parentfromnum):
		# -- live tv already started		
		if self.args.has_key('stop'):			
			self.stopLiveTvReaderThread()
			redirurl = '/%d?fromnum=%s' % (self.CHANNELS_FOLDER, parentfromnum)
			self.redirect(redirurl.encode('utf8'))					
		elif self.args.has_key('retune'):			
			self.stopLiveTvReaderThread()			
			redirurl = self.path
			self.redirect(redirurl.encode('utf8'))
		else:			
			basepath = self.channel.factory.basepaths[self.clientip]
			splitpath = basepath.split('/')
			basename = splitpath[-1]
			hrefplay = 'http://%s:%s/%s%s' % (self.hostname, self.port, self.LIVETV_PLAYBACK, basename)
			hrefrefresh = '%s?recid=%s&parentfromnum=%s' % (self.path, recid, parentfromnum)
			hrefstop = '%s?recid=%s&stop=true&parentfromnum=%s' % (self.path, recid, parentfromnum)
			hrefretune = '%s?recid=%s&retune=true&parentfromnum=%s' % (self.path, recid, parentfromnum)
			refreshpage = ''
			title = 'Start playback'
			buffcount = len(self.channel.factory.databytes[recid])
			descr = 'Got %d buffers (%d bytes)' % (buffcount, buffcount*self.BUFFERSIZE)
			mediaItems = [
		        { 'tvid': '1', 'name': title, 'href': hrefplay, 'descr': descr }
		    ]
			navItems = [					    
		        { 'tvid': '2', 'name': 'Refresh', 'href': hrefrefresh , 'descr': '' },
		        { 'tvid': '3', 'name': 'Back', 'href': hrefstop, 'descr': '' },
		        { 'tvid': '4', 'name': 'Retune', 'href': hrefretune, 'descr': '' } 
		    ]					
			onloadset = '1' if buffcount > 10 else '2'
			parenturl = '/%d' % self.CHANNELS_FOLDER					
			self.write(NewovPageXml(filename, title, mediaItems, navItems, {}, 
		                            parenturl, hrefrefresh, onloadset=onloadset).renderSynchronously())
							
			#basepath = MythRequestHandler.getCurrentRecording(self.mythbe, recid)
			#if basepath != None:		
			#	splitpath = basepath.split('/')
			#	basename = splitpath[-1]
			#	p = self.getRecordingWithBasename(basename)		
			#	percent = p.filesize / (self.BUFFERSIZE / 100)				
			#	now = datetime.datetime.now()				
			#	hrefplay = 'http://%s:%s/%s%s' % (self.hostname, self.port, self.LIVETV_PLAYBACK, basename)
			#	hrefrefresh = '%s?recid=%s&parentfromnum=%s' % (self.path, recid, parentfromnum)
			#	hrefstop = '%s?recid=%s&stop=true&parentfromnum=%s' % (self.path, recid, parentfromnum)
			#	hrefretune = '%s?recid=%s&retune=true&parentfromnum=%s' % (self.path, recid, parentfromnum)
			#	refreshpage = ''
			#	title = 'Start playback of channel %s' % (p.callsign)
			#	descr = 'Got %d percent (%d bytes)' % (percent, p.filesize)
			#	mediaItems = [
			#		{ 'tvid': '1', 'name': title, 'href': hrefplay, 'descr': descr }
			#	]
			#	navItems = [					    
			#		{ 'tvid': '2', 'name': 'Refresh', 'href': hrefrefresh , 'descr': '' },
			#		{ 'tvid': '3', 'name': 'Back', 'href': hrefstop, 'descr': '' },
			#		{ 'tvid': '4', 'name': 'Retune', 'href': hrefretune, 'descr': '' } 
			#	]					
			#	onloadset = '1' if percent > 100 else '2'
			#	parenturl = '/%d' % self.CHANNELS_FOLDER					
			#	self.write(NewovPageXml(filename, title, mediaItems, navItems, {}, 
			#					        parenturl, hrefrefresh, onloadset=onloadset).renderSynchronously())

	def processGuideFolder(self, filename):
		title = 'MythPoppy %s - Program Guide' % self.hostname
		# -- parse query string
		if self.args.has_key('fromnum'):
			fromnum = int(self.args['fromnum'][0])
		else:
			fromnum = 1
		if self.args.has_key('onloadset'):
			onloadset = self.args['onloadset'][0]
		else:
			onloadset = 'next'
		# -- prepare previous / next url
		tonum = fromnum + self.GUIDE_CHANNEL_INC
		if fromnum > self.GUIDE_CHANNEL_INC:
			previousurl = '%s?fromnum=%d&onloadset=previous' % (self.path, fromnum - self.GUIDE_CHANNEL_INC)
		else:
			previousurl = self.path
		nexturl =  '%s?fromnum=%d&onloadset=next' % (self.path, tonum)	
		mediaItems = []		
		(navItems, atend) = self.getChannels(self.GUIDE_FOLDER, fromnum, tonum, False)
		if atend:
			nexturl = '#'
		self.write(NewovPageXml(filename, title, mediaItems, navItems, {}, 
				                '/', self.path, previousurl, nexturl, onloadset).renderSynchronously())

	def processGuideItem(self, filename):
		# -- parse query string			
		if self.args.has_key('parentfromnum'):
			parentfromnum = int(self.args['parentfromnum'][0])
		else:
			parentfromnum = 1		
		if self.args.has_key('fromnum'):
			fromnum = int(self.args['fromnum'][0])
		else:
			fromnum = 1		
		if self.args.has_key('onloadset'):
			onloadset = self.args['onloadset'][0]
		else:
			onloadset = 'next'
		if self.args.has_key('chanid'):
			chanid = int(self.args['chanid'][0])
		else:
			chanid = 0			
		record = 0
		if self.args.has_key('record'):
			record = 1 if  self.args['record'][0].lower() == 'yes' else -1						
		# -- list of shows or show detail?
		title = 'MythPoppy %s - Program Guide - %s' % (self.hostname, self.getCallsign(chanid))
		splitpath = self.path.split('/')
		if len(splitpath) < 4:
			self.processGuideItemListOfShows(filename, title, chanid, fromnum, onloadset, parentfromnum)
		else:
			stime = splitpath[-1]
			channum = splitpath[-2]
			self.processGuideItemShowDetail(filename, title, chanid, channum, stime, record)

	def processGuideItemListOfShows(self, filename, title, chanid, fromnum, onloadset, parentfromnum):
		# -- list of shows
		parenturl = '/%d?fromnum=%d' % (self.GUIDE_FOLDER, parentfromnum)
		refreshurl = '%s?chanid=%d' % (self.path, chanid)
		tonum = fromnum + self.RECORDING_INC
		if fromnum > self.RECORDING_INC:
			previousurl = '%s?chanid=%d&fromnum=%d&onloadset=previous&parentfromnum=%d' % (self.path, 
						                                                                   chanid, fromnum - self.RECORDING_INC, parentfromnum)
		else:
			previousurl = '%s?chanid=%d&onloadset=previous&parentfromnum=%d' % (self.path,
						                                                        chanid, parentfromnum)
		nexturl = '%s?chanid=%d&fromnum=%d&onloadset=next&parentfromnum=%d' % (self.path, 
				                                                               chanid, tonum, parentfromnum)					
		mediaItems = [] 					
		(navItems, atend) = self.getGuide(chanid, fromnum, tonum)				
		if atend:
			nexturl = '#'
		self.write(NewovPageXml(filename, title, mediaItems, navItems, {}, 
				                parenturl, refreshurl, previousurl, nexturl, onloadset).renderSynchronously())

	def processGuideItemShowDetail(self, filename, title, chanid, channum, stime, record):
		# -- show detail
		parenturl = '/%d/%s?chanid=%d' % (self.GUIDE_FOLDER, channum, chanid)
		refreshurl = '%s?chanid=%d' % (self.path, chanid)
		previousurl = '#'
		nexturl = '#'
		mediaItems = None
		navItems = None
		onloadset = ''
		year = int(stime[0:4])								
		month = int(stime[4:6])				
		day = int(stime[6:8])
		hour = int(stime[8:10])
		minute = int(stime[10:12])								
		starttime = datetime.datetime(year, month, day, hour, minute)												
		# -- enable / disable recording
		if record > 0:
			# -- enable recording
			glst = self.mythdb.searchGuide(chanid=chanid, starttime=starttime)
			if glst != None and len(glst) > 0:		
				g = glst[0]
				g.record(Record.kSingleRecord)
			self.redirect(refreshurl)
		elif record < 0:
			# -- disable recording
			records = self.mythdb.searchRecord(chanid=chanid, starttime=starttime)
			if records != None:
				for record in records:
					record.delete()	
			self.redirect(refreshurl)
		else:
			# -- show detail
			mediaItems = []
			navItems = self.getShow(chanid, channum, starttime)
			onloadset = 'PLAY' if len(navItems) > 1 else ''
		if not (mediaItems is None or navItems is None):
			self.write(NewovPageXml(filename, title, mediaItems, navItems, {}, 
						            parenturl, refreshurl, previousurl, nexturl, onloadset).renderSynchronously())	

	def processRecordingsFolder(self, filename):
		title = 'MythPoppy %s - Recorded Shows' % self.hostname
		# -- parse query string			
		if not self.args.has_key('fromnum'):
			fromnum = 1				
		else:
			fromnum = int(self.args['fromnum'][0])
		if not self.args.has_key('onloadset'):
			onloadset = 'next'
		else:
			onloadset = self.args['onloadset'][0]
		# -- prepare colorbutton navigation url			
		tonum = fromnum + self.RECORDING_INC
		if fromnum > self.RECORDING_INC:
			previousurl = '%s?fromnum=%d&onloadset=previous' % (self.path, fromnum - self.RECORDING_INC)
		else:
			previousurl = self.path
		nexturl =  '%s?fromnum=%d&onloadset=next' % (self.path, tonum)			
		# -- get recordings and render page
		(mediaItems, atend) = self.getRecordings(fromnum, tonum)	
		if atend: 
			nexturl = '#'
		navItems = []
		self.write(NewovPageXml(filename, title, mediaItems, navItems, {}, 
				                '/', self.path, previousurl, nexturl, onloadset).renderSynchronously())

	def processScheduledFolder(self, filename):
		title = 'MythPoppy %s - Scheduled Recordings' % self.hostname
		# -- parse query string
		if self.args.has_key('chanid'):
			chanid = int(self.args['chanid'][0])
		else:
			chanid = None
		if self.args.has_key('stime'):
			stime = self.args['stime'][0]
		else:
			stime = None
		if not self.args.has_key('fromnum'):
			fromnum = 1				
		else:
			fromnum = int(self.args['fromnum'][0])
		if not self.args.has_key('onloadset'):
			onloadset = 'next'
		else:
			onloadset = self.args['onloadset'][0]
		# -- prepare previous / next url
		tonum = fromnum + self.SCHEDULED_INC
		if fromnum > self.SCHEDULED_INC:
			previousurl = '%s?fromnum=%d&onloadset=previous' % (self.path, fromnum - self.SCHEDULED_INC)
		else:
			previousurl = self.path
		nexturl =  '%s?fromnum=%d&onloadset=next' % (self.path, tonum)
		if not (chanid is None or stime is None):
			styear = int(stime[0:4])								
			stmonth = int(stime[4:6])				
			stday = int(stime[6:8])
			sthour = int(stime[8:10])
			stminute = int(stime[10:12])
			starttime = datetime.datetime(styear, stmonth, stday, sthour, stminute)				
			records = self.mythdb.searchRecord(chanid=chanid, starttime=starttime) 				
			if records != None:
				for record in records:						
					record.delete()	
			self.redirect(self.path)
		else:
			mediaItems = []	
			(navItems, atend) = self.getScheduled(fromnum, tonum)
			if atend:
				nexturl = '#'
			self.write(NewovPageXml(filename, title, mediaItems, navItems, {}, 
						            '/', self.path, previousurl, nexturl, onloadset).renderSynchronously())

	def processResertBackend(self):
		subprocess.call('./resetmythbackend.sh')			
		# -- reconnect to backend
		self.mythbe = self.channel.factory.mythbe = MythBE()
		self.mythdb = self.channel.factory.mythdb = MythDB()
		self.redirect('/')
	
	def processLiveTvPlayback(self):
		recid = None		
		# -- range header
		headers = self.getAllHeaders()				
		(rangestart, rangeend) = self.getRangeStartAndEnd(headers)		
		# -- check recid
		if not self.channel.factory.recids.has_key(self.clientip):
			redirurl = '/%d' % self.CHANNELS_FOLDER		
			self.redirect(redirurl.encode('utf8'))
		else:	
			# -- get recid
			recid = self.channel.factory.recids[self.clientip]
			count= 0
			timeout= 30
			chunksize = 0			
			while len(self.channel.factory.databytes[recid]) == 0:				
				# -- not twisted style, sorry				
				if logLevel > 1: log.msg('sleeping, waiting for data %d' % count)
				time.sleep(1)				
				count += 1
				if count > timeout:
					break;

			if logLevel > 1: log.msg('len databytes %s' % len(self.channel.factory.databytes[recid]))
			if len(self.channel.factory.databytes[recid]) > 0:									
				if rangestart == 0:			
					buff = self.channel.factory.databytes[recid].pop(0)
					self.channel.factory.initbytes[recid].append(buff)
					chunksize = self.writeChunks(self.channel.factory.initbytes[recid], rangestart)			
					self.channel.factory.byteswritten[recid] = chunksize
				else:				
					self.channel.factory.initbytes[recid] = []
					buff = self.channel.factory.databytes[recid].pop(0)					
					chunksize = self.writeChunk(buff, rangestart)					
					self.channel.factory.byteswritten[recid] += chunksize

			if logLevel > 1: 
				log.msg('rangestart %s, chunksize %s, byteswritten %s' % 
						(rangestart, chunksize, self.channel.factory.byteswritten[recid]))			
		return	
	
	def processRecordingPlayback(self):
		
		# -- range header
		headers = self.getAllHeaders()				
		(rangestart, rangeend) = self.getRangeStartAndEnd(headers)	

		# -- check basepath
		if not self.channel.factory.basepaths.has_key(self.clientip):		
			# -- get basename
			splitpath = self.path.split('/')		
			basenametemp = splitpath[-1]		
			basename = ''
			# -- check basename
			if not basenametemp.startswith(self.RECORDING_PLAYBACK):
				redirurl = '/%d' % self.CHANNELS_FOLDER		
				self.redirect(redirurl.encode('utf8'))
			else:		
				basename = basenametemp[len(self.RECORDING_PLAYBACK):]
				basepath = '%s%s' % (self.recRootPath, basename)
				self.channel.factory.basepaths[self.clientip] = basepath	
				
		# -- get basepath and file size
		basepath = self.channel.factory.basepaths[self.clientip]		
		
		# -- read chunk
		recFile = open(basepath, 'r')
		recFile.seek(rangestart)
		buff = recFile.read(self.RECBUFSIZE)
		chunksize = self.writeChunk(buff, rangestart)
		recFile.close()
		return	
	
	def processXml(self, filename):						
		
		# -- check for running live tv		
		if not self.args.has_key('recid'):
			self.stopLiveTvReaderThread()
			
		# -- where does the user want to go?
		if self.path == '/':		
			self.processRoot(filename)
		elif self.path == ('/%d' % self.CHANNELS_FOLDER):
			self.processChannelsFolder(filename)
		elif self.path.startswith('/%d/' % self.CHANNELS_FOLDER):
			self.processChannelItem(filename)
		elif self.path == ('/%d' % self.GUIDE_FOLDER):
			self.processGuideFolder(filename)
		elif self.path.startswith('/%d/' % self.GUIDE_FOLDER):						
			self.processGuideItem(filename)
		elif self.path == ('/%d' % self.RECORDINGS_FOLDER):
			self.processRecordingsFolder(filename)
		#elif self.path == ('/%d' % self.VIDEOS_FOLDER):
		#	self.write('<h1>Videos</h1>Videos page: ' + str(self.args))		
		elif self.path == ('/%d' % self.SCHEDULED_FOLDER):
			self.processScheduledFolder(filename)
		elif self.path == ('/%d' % self.RESET_BACKEND):
			self.processResertBackend()
		else:
			self.setResponseCode(http.NOT_FOUND)
			self.write('<h1>Not Found</h1>Sorry, no such page')

	def process(self):
		try:						
			# -- save references to useful resources			
			self.clientip = self.getClientIP()
			self.port = self.channel.factory.port
			self.hostname = self.channel.factory.hostname
			self.mythbe = self.channel.factory.mythbe
			self.mythdb = self.channel.factory.mythdb
			self.recPreference = self.channel.factory.recPreference	
			self.recRootPath = self.channel.factory.recRootPath
			if not self.recRootPath.endswith('/'): self.recRootPath += '/'
			self.getCallsign = self.channel.factory.getCallsign
			# -- get the filename
			filepath = self.path.split('/')	
			filename = filepath[-1]			
			if len(filename.split('.')) == 1:
				filename = 'mythpoppy.xml'
			# -- primitive logging
			if logLevel > 1: log.msg('%s %s' % (str(self), filename))
			# -- serve the file
			if os.path.exists(filename):
				if filename.lower().endswith('.xml'):
					self.processXml(filename)				
				else:
					f = open(filename, 'rb')
					buff = f.read()
					f.close()
					self.write(buff)		
			elif filename.lower().startswith(self.LIVETV_PLAYBACK):
				self.processLiveTvPlayback()
			elif filename.lower().startswith(self.RECORDING_PLAYBACK) and filename.lower().endswith(".mpg"):
				self.processRecordingPlayback()
			else:
				self.setResponseCode(http.NOT_FOUND)
				self.write('<h1>Not found</h1>Sorry, no such page.')			
		except Exception as err:
			log.err(err)
			self.setResponseCode(http.INTERNAL_SERVER_ERROR)
			self.write('<h1>Internal Server Error</h1>Error: %s' % err)
		self.finish()

class MythHttpProtocol(http.HTTPChannel):
	requestFactory= MythRequestHandler

class MythPoppyHttpFactory(http.HTTPFactory):				
	mythdb = MythDB()
	mythbe = MythBE()	
	protocol = MythHttpProtocol
	hostname = socket.gethostname()	
	port = None
	recids = {}	
	callsigns = {}
	byteswritten = {}
	basepaths = {}
	initbytes = {}
	databytes = {}	
	readthreads = {}
	callsignCacheUpdate = datetime.datetime.min
	callsignCacheUpdateInterval = datetime.timedelta(0, 300)

	def __init__(self, port):
		# -- TCP port
		self.port = port
		# -- read configuration file
		configParser = SafeConfigParser()		
		configParser.read('/etc/mythpoppy.conf')
		if configParser.has_section('mythpoppy'):			
			print 'Got configuration from /etc/mythpoppy.conf'			
		else:			
			configParser.read('./mythpoppy.conf')
			if configParser.has_section('mythpoppy'):
				print 'Got configuration from /etc/mythpoppy.conf'
			else:
				raise 'Could not find config file /etc/mythpoppy.conf or ./mythpoppy.conf'					
		# -- read and parse options		
		recPreference = configParser.get('mythpoppy', 'recorder_preference')
		self.recPreference = map(int, recPreference.split(','))			
		self.recRootPath = configParser.get('mythpoppy', 'recordings_root_path')
		if self.recRootPath[-1] != '/':
			self.recRootPath += '/'		

	def doStop(self):		
		# -- stop reader threads
		for clientip in self.readthreads:
			self.readthreads[clientip].stop()
			self.readthreads[clientip].join()
		# -- stop live tv
		#for recid in self.recids:
		#	MythRequestHandler.stopLiveTv(self.mythbe, recid)			
		# -- call base function
		http.HTTPFactory.doStop(self)

	def getCallsign(self, recid):		
		now = datetime.datetime.now()
		delta = now - self.callsignCacheUpdate		
		if delta > self.callsignCacheUpdateInterval:
			self.callsignCacheUpdate = now
			# -- lookup dictionary for callsigns
			callsigns = {}	
			ch = self.mythdb.getChannels()
			callsigns[0] = 'Unknown Channel'
			for c in ch:
				callsigns[c.chanid] = c.callsign
			self.callsigns = callsigns			
		callsign = 'Unknown'
		if self.callsigns.has_key(recid):
			callsign = self.callsigns[recid]
		return callsign	

class ReadThread(threading.Thread):

	# -- "constant" attributes
	BACKEND_SEP = '[]:[]'
	MAX_CHUNKS = 200

	# -- "regular" attributes
	factory = None
	clientip = None
	recid = None
	buffsize = None
	stopRequest = False
	recFile = None

	def __init__(self, factory, clientip, recid, buffsize):
		self.factory = factory
		self.clientip = clientip
		self.recid = recid
		self.buffsize = buffsize
		threading.Thread.__init__(self)

	def stop(self):		
		self.stopRequest = True

	def run(self):
		if logLevel > 1: log.msg('Starting reader thread')
		#mythbe = MythBE()
		mythbe = self.factory.mythbe
		basepath = MythRequestHandler.getCurrentRecording(mythbe, self.recid)		
		self.recFile = open(basepath, 'r')
		bytesread = 0
		emptybasepathcount = 0
		while not self.stopRequest:	
			# -- don't use too much memory buffer
			if len(self.factory.databytes[self.recid]) >= self.MAX_CHUNKS:
				time.sleep(1)
			else:
				# -- check if next chunk of content can be read
				if os.path.getsize(basepath) >= (bytesread + self.buffsize):				
					# -- read next chunk of content
					buff = self.recFile.read(self.buffsize)
					self.factory.databytes[self.recid].append(buff)
					bytesread += len(buff)
					if logLevel > 2: log.msg('Read %s bytes, %d chunks in buffer' % (len(buff), len(self.factory.databytes[self.recid])))			
				else:
					# -- check for new program
					basepathtemp = MythRequestHandler.getCurrentRecording(mythbe, self.recid)
					if basepathtemp == None or basepathtemp == '':
						emptybasepathcount += 1						
						if emptybasepathcount > 30:
							# -- abort
							self.stopRequest = True
							continue
						# -- wait for content
						time.sleep(1)
					elif basepathtemp == basepath:
						# -- wait for content
						time.sleep(1)
					else:				
						emptybasepathcount = 0
						# -- different basepath means new program					
						if logLevel > 1: log.msg('New basepath %s' % basepathtemp)						
						# -- read rest of old file
						while os.path.getsize(basepath) >= (bytesread + self.buffsize):
							buff = self.recFile.read(self.buffsize)
							self.factory.databytes[self.recid].append(buff)
							bytesread += len(buff)							
						# -- read last bytes of old file
						buff = self.recFile.read(self.buffsize)
						self.recFile.close()
						self.recFile = None						
						# -- wait for new file
						basepath = MythRequestHandler.waitForStableBasepath(mythbe, self.recid, interval=1, timeout=30)
						if basepath == None or basepath == '':
							# -- abort
							self.stopRequest = True
							continue
						else:
							self.recFile = open(basepath, 'r')
							if logLevel > 1: log.msg('Opened new basepath %s' % basepath)
							bytestoread = self.buffsize - len(buff)							
							# -- wait for content in new file
							timeout = 0
							while os.path.getsize(basepath) < bytestoread and timeout < 30:
								time.sleep(1)
								timeout += 1							
							if os.path.getsize(basepath) >= bytestoread:
								buff2 = self.recFile.read(bytestoread)
								self.factory.databytes[self.recid].append(buff + buff2)
								bytesread = len(buff2)
							else:								
								# -- abort
								self.stopRequest = True
								continue						
		if self.recFile != None:
			self.recFile.close()
			MythRequestHandler.stopLiveTv(mythbe, self.recid)			
		if logLevel > 1: log.msg('Stopping reader thread')	

class MythPoppyService(internet.TCPServer):
	def __init__(self):
		port = 8121
		internet.TCPServer.__init__(self, port, MythPoppyHttpFactory(port))		

if __name__ == '__main__':
	print 'start for production:'
	print '\ttwistd -y %s --pidfile /var/tmp/mythpoppy.pid --logfile /var/log/mythpoppy.log' % sys.argv[0]
	print 'start for testing:'
	print '\ttwistd -noy %s --pidfile ./mythpoppy.pid' % sys.argv[0]
	sys.exit(-1)
else:
	# -- mythtv user, run 'id -u mythtv' to find uid
	#UID = 115 
	UID = 1000 
	# -- mythtv group, run 'id -g mythtv' to find gid
	#GID = 123
	GID = 1000	
	application = service.Application("MythPoppy")	
	mythpoppyService = MythPoppyService()
	mythpoppyService.setServiceParent(application)	